#!/bin/sh
# vim:ts=4:sw=4:noexpandtab
#
# Parallels Desktop dispatcher, network and kext-s.
#
# Copyright (c) 2004-2014 Parallels IP Holdings GmbH.
# All rights reserved.
# http://www.parallels.com


export PATH=/bin:/sbin:/usr/bin:/usr/sbin

MACOSX_VERSION="$(sw_vers -productVersion)"
MACOSX_10_7_X_EXT_WORKAROUND="$(echo ${MACOSX_VERSION} | sed 's:^10.7\(.[[:digit:]]\)*$:YES:')"

BundleDir=$(cd "`dirname "${0}"`/../.."; pwd)
BundleContentsDir="${BundleDir}/Contents"
BundleDirOwners=$(stat -f '%Su:%Sg' "${BundleDir}")

SYS_KEXT_DIR="/System/Library/Extensions/"
KEXT_DIR="${BundleContentsDir}/Library/Extensions"
UTILITIES_DIR="${BundleContentsDir}/MacOS"
ParallelsWatchdog="${BundleContentsDir}/MacOS/watchdog"
ParallelsDispatcherService="${BundleContentsDir}/MacOS/Parallels Service.app/Contents/MacOS/prl_disp_service"
ServicePath="${BundleContentsDir}"
ParallelsNetStart="${ServicePath}/MacOS/prl_net_start"
ParallelsNatService="${ServicePath}/MacOS/prl_naptd"
DispatcherAppName='prl_disp_service'
NatAppName='prl_naptd'
NatWatchdogTimeout=60 # seconds
NatWatchdogLimit=20 # times
SystemMajorVersion=`sw_vers -productVersion | cut -d . -f 2`

LIGHTWEIGHT_MODE_FLAG="${BundleDir}/Contents/Resources/.lightweight"

# Warning: fix ParallelsDirs if this path changes
if [ "${SystemMajorVersion}" -ge "9" ]; then
	KEXT_DIR="${KEXT_DIR}/10.9"
elif [ "${SystemMajorVersion}" -ge "7" ]; then
	KEXT_DIR="${KEXT_DIR}/10.7"
fi

KernelExtensionHypervisor="${KEXT_DIR}/prl_hypervisor.kext"
KernelExtensionUsbConnect="${KEXT_DIR}/prl_usb_connect.kext"
KernelExtensionVnic="${KEXT_DIR}/prl_vnic.kext"
KernelExtensionNetbridge="${KEXT_DIR}/prl_netbridge.kext"

AUTHORITY="Authority=Developer ID Application: Parallels, Inc."

LaunchdMode="launchd-mode"

LAUNCHD_SETUP="${UTILITIES_DIR}/launchd_setup"

RT_ROOT=
DispatcherInitLockFile=
LogFile=
DispatcherPidFile=
NatPidFile=


make_rt_dirs() {
	local tmp_dir='/tmp/.pd'
	RT_ROOT="${tmp_dir}/`id -u`"
	mkdir -p "${RT_ROOT}"/{tmp,var/{run,log,tmp}} || echo "mkdir -p failed"
	chmod a+rwxt "${tmp_dir}" 2>/dev/null
}

init_runtime() {
	local uid=$(id -u)
	[ ${uid} -ne 0 ] && make_rt_dirs
	DispatcherInitLockFile="${RT_ROOT}/var/run/prl_disp_service.prepared"
	LogFile="${RT_ROOT}/var/log/prl_disp_service_server.log"
	DispatcherPidFile="${RT_ROOT}/var/run/prl_disp_service.pid"
	NatPidFile="${RT_ROOT}/var/run/prl_naptd.pid"
}

log() {
	logger -t 'Parallels' "$@"
	# Add entry to Parallels log file
	echo "`date +"%b %d %H:%M:%S    "` $@" >> "${LogFile}"
}

is_sandbox() {
	[ -f "${LIGHTWEIGHT_MODE_FLAG}" ]
}

check_util() {
	if [ ! -x "${1}" ]; then
		log "'${1}' is missed or not an executable"
		return 1
	fi
	return 0
}

check_file_sign() {
	local file="${1}"
	local cs=/usr/bin/codesign
	local gr=/usr/bin/grep

	check_util "${cs}" || return 1
	check_util "${gr}" || return 1

	"${cs}" --verify "${file}" &&
		"${cs}" --display --verbose=2 --requirements - "${file}" 2>&1 |
		"${gr}" -q "${AUTHORITY}"
}

lock_disp_init() {
	if [ ! -f "${DispatcherInitLockFile}" ]; then
		return
	fi

	rm -f "${DispatcherInitLockFile}" || log "Unable to remove dispatcher lock"
}

unlock_disp_init() {
	touch "${DispatcherInitLockFile}" || log "Unable to create dispatcher unlock"
}

wait_for_stop() {
	local timeout=$1
	local sleep_time=1
	local curr_wait=0
	local rc=0
	log "Wait until dispatcher stops with timeout in $timeout secs."
	while true; do
			local pid=$(get_pid "${DispatcherAppName}" "${DispatcherPidFile}")
			[ -z "${pid}" ] && break

			sleep $sleep_time

			let "curr_wait += $sleep_time"
			if [ "$curr_wait" -gt "$timeout" ]; then
				log "Timeout expired. Unable to wait dispatcher stop."
				rc=1
				break
			fi
	done
	log "Dispatcher was stopped in $curr_wait secs"
	return $rc
}

#  The kernel extension loading routine
#  Parameters are:
#  $1   kext filename (e.g. prl_hypervisor.kext)
#       mandatory
#  $2   if set to 'yes' then symlink to respective kext will be created
#       under /System/Library/Extensions/ before kext load.
load_kext() {
	local KextMod="${1}"
	local CreateSymlink="${2-no}"
	local KextModName=`basename "${KextMod}"`

	log "Loading kernel extension ${KextModName}"
	if [ "${MACOSX_10_7_X_EXT_WORKAROUND}" = "YES" ] && \
	 [ "${CreateSymlink}" == 'yes' ]; then
		log "Mac OS X 10.7.X workaround: creating symlinks"
		local KextLink="$SYS_KEXT_DIR/${KextModName}"
		if [ ! -L "${KextLink}" ] ||
			[ "$(readlink "${KextLink}")" != "${KextMod}" ]; then
			test -d "${KextMod}" && ln -sfh "${KextMod}" "${KextLink}"
		fi
	fi

	local out=`kextload -r "$KEXT_DIR" "${KextMod}" 2>&1`
	if [ "x${?}" != "x0" ]; then
		log "Failed to load kernel extension ${KextModName}"
		log "${out}"
		return 1
	fi
}

rm_kext_symlink() {
	local KextMod="${1}"
	local KextModName=`basename "${KextMod}"`
	local KextPath=${SYS_KEXT_DIR}/${KextModName}

	if [ -e "$KextPath" ]
	then
		log "Removing reference for ${KextModName} from ${SYS_KEXT_DIR}"
		rm -rf "${KextPath}"
	fi
}

unload_kext() {
	local kext_path=${1}
	local kext=`basename "${kext_path}"`

	local kext_plist=${kext_path}/Contents/Info.plist
	local kext_id=`defaults read "${kext_plist}" CFBundleIdentifier`
	log "Going to unload kernel extension ${kext} (${kext_id})"

	local num_tries=3
	local try=1
	local rc=0
	while true; do
		if ! kextstat | grep -Fqs "${kext_id}"; then
			[ ${try} -eq 1 ] && log " - kext ${kext} not loaded"
			break
		fi
		if [ ${try} -gt ${num_tries} ]; then
			rc=1
			log "Failed to unload kext ${kext}"
			break
		fi
		log " - unloading ${kext} (${try}/${num_tries})"
		kextunload "${kext_path}"
		((++try))
	done

	[ ${rc} -eq 0 ] && rm_kext_symlink "${kext_path}"
	return $rc
}

warm_up() {
	local ParallelsVm="${BundleContentsDir}/MacOS/Parallels VM.app/Contents/MacOS/prl_vm_app"
	if [ -f "${ParallelsVm}" ]; then
		"${ParallelsVm}" --mode cache >/dev/null 2>&1 &
	fi
}

prepare_start_net() {
	log "Starting Parallels networking..."
	"${ParallelsNetStart}"
	if [ "x${?}" != "x0" ]; then
		log "Failed to start Parallels networking"
		exit 1
	fi
}

start_net() {
	"${ParallelsWatchdog}" start "${NatWatchdogTimeout}" "${NatWatchdogLimit}" "${ParallelsNatService}" start >/dev/null 2>&1 &
	log 'Parallels networking sucessfully started'

	log "Restarting CiscoVPN"
	SystemStarter restart CiscoVPN
}

stop_net() {
	log "Stopping Parallels networking"
	local pid=$(get_pid "${NatAppName}" "${NatPidFile}")
	if [ -z "$pid" ]; then
		log error "Cannot find pid for ${ParallelsNatService}"
		return
	fi
	"${ParallelsWatchdog}" kill "${ParallelsNatService}" "${pid}" >/dev/null 2>&1
	log "Parallels Networking was stopped with retcode $?"
}

# Return requested PID
# Delete PID file if process isn't running
get_pid() {
	local ProcessName="${1}"
	local PidFile="${2}"
	local pid=""
	if [ -f "${PidFile}" ]; then
		pid=$(head -n 1 "${PidFile}")
		# check that it is our process && check for zombi
		local proc_name_part="`lsof -p ${pid} | tail -n 1| cut -f 1 -d' '  `"
		echo "${ProcessName}" | grep -q "^${proc_name_part}" && kill -0 "${pid}" 2>&1 > /dev/null
		if [ $? -ne 0 ]; then
			rm -f "${PidFile}"
			pid=""
		fi
	fi

	if [ -n "${pid}" ]; then
		echo ${pid}
		return 0
	else
		return 1
	fi
}

start_disp() {
	log "Starting Parallels Dispatcher Service"

	# Check for running dispatcher
	local pid=$(get_pid "${DispatcherAppName}" "${DispatcherPidFile}")
	if [ -n "${pid}" ]; then
		log "Parallels Dispatcher Service is already running (pid:${pid})"
		return
	fi

	local launchd_param=
	if [ x"$1" == x"${LaunchdMode}" ]; then
		launchd_param="--launchd-mode"
	fi

	#  Run Parallels dispatcher service
	check_file_sign "${ParallelsDispatcherService}"
	if [ "x${?}" != "x0" ]; then
		log "Wrong signature of Parallels Dispatcher Service"
		exit 1
	fi

	"${ParallelsDispatcherService}" -e --logfile "${LogFile}" --pidfile "${DispatcherPidFile}" "${launchd_param}" >/dev/null 2>&1 &

	if [ "x${?}" != "x0" ]; then
		log "Failed to start Parallels Dispatcher Service"
		exit 1
	fi

	log 'Parallels Dispatcher Service sucessfully started'
}

stop_disp() {
	log "Stopping Parallels Dispatcher Service"

	local pid=`cat ${DispatcherPidFile} 2>/dev/null`
	if [ $? -ne 0 ]; then
		return 1
	fi
	log "Parallels Dispatcher pid=${pid}"

	local rc=1
	if ! is_sandbox; then
		"${UTILITIES_DIR}/prlsrvctl" shutdown --force
		rc=$?
	fi

	if [ $rc -ne 0 ]; then
		kill -TERM "${pid}" 2>/dev/null
		wait_for_stop 120 # wait for 120 secs
	fi
}

load_all_kexts() {
	# Strict dependence on 64-bit kernel
	local arch=`uname -m`
	if [ "${arch}" != 'x86_64' ]; then
		log  "Unsupported kernel architecture: ${arch}"
		return 1
	fi

	# Kext-s must have root:wheel owner.
	log "Set 'root:wheel' as owners for ${KEXT_DIR}"
	chown -R root:wheel "${KEXT_DIR}"
	log "Set correct permissions for ${KEXT_DIR}"
	find "${KEXT_DIR}" -type d -exec chmod 0755 "{}" \;
	find "${KEXT_DIR}" -type f -exec chmod 0644 "{}" \;

	local exitstatus=0
	load_kext "${KernelExtensionUsbConnect}" 'yes'
	exitstatus=`expr $exitstatus + $?`
	load_kext "${KernelExtensionHypervisor}"
	exitstatus=`expr $exitstatus + $?`
	# netbridge must be loaded after hypervisor
	load_kext "${KernelExtensionNetbridge}"
	exitstatus=`expr $exitstatus + $?`
	load_kext "${KernelExtensionVnic}"
	exitstatus=`expr $exitstatus + $?`

	return $exitstatus
}

unload_all_kexts() {
	local exitstatus=0
	# netbridge must be unloaded before hypervisor
	unload_kext "${KernelExtensionNetbridge}"
	exitstatus=`expr $exitstatus + $?`
	unload_kext "${KernelExtensionVnic}"
	exitstatus=`expr $exitstatus + $?`
	unload_kext "${KernelExtensionUsbConnect}"
	exitstatus=`expr $exitstatus + $?`
	unload_kext "${KernelExtensionHypervisor}"
	exitstatus=`expr $exitstatus + $?`

	# Reset kexts owners to bundle owners
	log "Reset owners for ${KEXT_DIR}"
	chown -R "${BundleDirOwners}" "${KEXT_DIR}"

	return $exitstatus
}

# To make future generations life easier:
# this function actualizes symlimks to command-line tools in bundle from
# execution directory (/usr/bin currently).
install_cli_links() {
	local CLI='prlctl:prlsrvctl:prl_disk_tool:prl_perf_ctl:prl_convert'
	local SrcPath="${BundleContentsDir}/MacOS"
	local TgtPath='/usr/bin'

	IFS=':'
	for Tool in $CLI; do
		local SrcTool="${SrcPath}/parallels_wrapper"
		local TgtTool="${TgtPath}/${Tool}"
		if [ -L "${TgtTool}" ]; then
			test "x`readlink \"${TgtTool}\"`" != "x${SrcTool}" && rm -f "${TgtTool}"
		fi
		[ -e "${TgtTool}" ] || ln -s "${SrcTool}" "${TgtTool}"
	done
	unset IFS
}

# Prl man files (prlctl.8, etc) are copied to the bundle to some God forsaken
# place, which no one can locate. This function adds symbolic links from that
# place to widely known /usr/share/man (where users expect them to be).
install_man_links() {
	local MAN='prlctl.8:prlsrvctl.8:prl_disk_tool.8:prl_convert.8'
	local ManSrcPath="${BundleContentsDir}/Frameworks/ParallelsVirtualizationSDK.framework/Versions/Current/Resources/English.lproj"
	local ManTgtPath='/usr/share/man/man8'

	IFS=":"
	for Man in $MAN; do
		local SrcMan="${ManSrcPath}/${Man}"
		local TgtMan="${ManTgtPath}/${Man}"
		if [ -L "${TgtMan}" ]; then
			test "x`readlink \"${TgtMan}\"`" != "x${SrcMan}" && rm -f "${TgtMan}"
		fi
		[ -e "${TgtMan}" ] || ln -s "${SrcMan}" "${TgtMan}"
	done
	unset IFS
}

start_service() {
	local rc=0

	if ! is_sandbox; then
		install_cli_links &
		install_man_links &
	fi

	lock_disp_init
	start_disp "$1"

	if ! is_sandbox; then
		load_all_kexts
		rc=$?
		log "Loading kernel extensions exit status: ${rc}"
	fi

	[ ${rc} -eq 0 ] && prepare_start_net

	# allow to continue dispatcher initialization
	unlock_disp_init

	[ ${rc} -eq 0 ] && start_net

	# Prepare binaries for start
	warm_up

	# Do wait when it was started by launchd
	if [ x"$1" == x"${LaunchdMode}" ]; then
		trap 'stop_service;exit 0' TERM

		if pid=`cat ${DispatcherPidFile}`; then
			wait "$pid"
			log 'Parallels Dispatcher Service was stopped'
		fi
	fi

	return $rc
}

stop_service() {
	stop_disp
	stop_net
	if ! is_sandbox; then
		unload_all_kexts
	fi
}

check_launchd() {
	local RC=0
	if "${LAUNCHD_SETUP}" need_to_install; then
		"${LAUNCHD_SETUP}" install --force
		RC=$?
	elif "${LAUNCHD_SETUP}" need_to_uninstall; then
		"${LAUNCHD_SETUP}" uninstall
		RC=$?
	fi
	return $RC
}

start_service_sba() {
	local RC=0

	if is_sandbox; then
		start_service
		return $?
	fi

	if ! "${LAUNCHD_SETUP}" need_to_start_as_service; then
		start_service
		return $?
	fi

	if "${LAUNCHD_SETUP}" need_to_install; then
		"${LAUNCHD_SETUP}" install --force
		RC=$?
	elif ! "${LAUNCHD_SETUP}" is_service_started; then
		"${LAUNCHD_SETUP}" start_service
		RC=$?
	else
		# do nothing - service is already started
		RC=0
	fi

	return $RC
}


init_runtime

RC=0
case $1 in
	start_sba)
		start_service_sba
		RC=$?
	;;
	start)
		start_service "${LaunchdMode}"
		RC=$?
	;;
	stop)
		stop_service
		RC=$?
	;;
	check_launchd)
		check_launchd
		RC=$?
	;;
	restart)
		stop_service; start_service
		RC=$?
	;;
	kext_load)
		load_all_kexts
		RC=$?
	;;
	kext_unload)
		unload_all_kexts
		RC=$?
	;;

	stop-net)
		stop_net
		RC=$?
	;;
esac

exit $RC
